package com.liebaut.controller;

import com.liebaut.exception.JsonError;
import com.liebaut.exception.RestException;
import com.liebaut.model.DomainObject;
import com.liebaut.services.BeanService;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.map.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;

import static com.liebaut.util.MapUtil.map;

@Controller
@Transactional
public abstract class CrudController<T extends DomainObject<ID>, ID extends Serializable> {
    private static final Logger log = LoggerFactory.getLogger(CrudController.class);

    protected JpaRepository<T, ID> repository;
    protected Class domainClass;

    @Autowired
    BeanService beanService;

    public CrudController() {}

    public CrudController(JpaRepository<T, ID> repository) {
        this.repository = repository;
        this.domainClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

    @RequestMapping(value = "", method = RequestMethod.GET)
    @ResponseBody
    public Iterable list(HttpServletRequest request) {
        String page = request.getParameter("page");
        String size = request.getParameter("size");
        if(page == null && size == null)
            return repository.findAll();
        int pageNumber = page == null ? 0 : Integer.parseInt(page);
        int sizeNumber = size == null ? 10 : Integer.parseInt(size);
        return repository.findAll(new PageRequest(pageNumber, sizeNumber));
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    @ResponseBody
    public <K> K getById(@PathVariable ID id) {
        T res = repository.findOne(id);
        if (res != null)
            return (K) res;
        else
            throw new RestException(HttpStatus.NOT_FOUND, "ID not found: " + id);
    }

    @RequestMapping(value = "", method = RequestMethod.POST, consumes = "application/json")
    @ResponseBody
    public Object save(@RequestBody String body) throws IOException {
        T entity;
        try {
            entity = saveRequestToEntity(body);
        } catch (JsonProcessingException e) {
            log.debug("invalid request", e);
            throw new HttpMessageNotReadableException("Invalid request", e);
        }
        repository.save(entity);
        return entity;
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    @ResponseBody
    public Object delete(@PathVariable ID id) {
        T entity = repository.findOne(id);
        if (entity != null){
            repository.delete(entity);
            return map("deleted", true);
        }
        else
            throw new RestException(HttpStatus.NOT_FOUND, "ID not found: " + id);
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PATCH)
    @ResponseBody
    public Object patch(@PathVariable("id") ID id, @RequestBody JsonPatchOperation[] operations) throws IOException {
        T entity = repository.findOne(id);
        if (entity == null)
            throw new RestException(HttpStatus.NOT_FOUND, "ID not found: " + id);
        return patchEntity(entity, operations);
    }

    protected Object patchEntity(T entity, JsonPatchOperation[] operations) {
        for (JsonPatchOperation oper : operations) {
            switch (oper.op) {
                case add:
                case replace:
                    beanService.setAttribute(entity, oper.path, oper.value);
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported operation: " + oper.op);
            }
        }
        repository.save(entity);
        return entity;
    }

    protected T saveRequestToEntity(String body) throws IOException {
        return (T) new ObjectMapper().readValue(body, domainClass);
    }

    @ExceptionHandler (Exception.class)
    @ResponseStatus (HttpStatus.INTERNAL_SERVER_ERROR)
    public ModelAndView handleAllExceptions(Exception ex) {
        return new JsonError(ex.getMessage()).asModelAndView();
    }

    @ExceptionHandler (IOException.class)
    @ResponseStatus (HttpStatus.BAD_REQUEST)
    public ModelAndView handleIOExceptions(Exception ex) {
        return new JsonError(ex.getMessage()).asModelAndView();
    }
}


